package com.javabi.sizeof.definition;

import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import com.javabi.sizeof.ClassDefinition;
import com.javabi.sizeof.ClassDefinitionMap;
import com.javabi.sizeof.MemoryUtil;

/**
 * An Object Definition.
 */
@SuppressWarnings("unchecked")
public final class ObjectDefinition<I> extends ClassDefinition<I> {

	/** The class. */
	private final Class<I> clazz;
	/** The size. */
	private final long size;
	/** The object fields. */
	private final Field[] objectFields;
	/** The primitive fields. */
	private final Field[] primitiveFields;
	/** Linked definition. */
	private ClassDefinition link = null;

	/**
	 * Creates a new definition.
	 * @param clazz the class.
	 * @param definitionMap the definition map.
	 */
	public ObjectDefinition(Class<I> clazz, ClassDefinitionMap definitionMap) {
		this.clazz = clazz;

		// Calculate size & find fields
		long size = MemoryUtil.SIZE_OF_OBJECT_INSTANCE;
		List<Field> objectFieldList = new ArrayList<Field>();
		List<Field> primitiveFieldList = new ArrayList<Field>();
		Class<?> type = clazz;
		while (!type.equals(Object.class)) {

			// Link existing definition
			if (definitionMap.contains(type)) {
				ClassDefinition<?> definition = definitionMap.get(type);
				if (!(definition instanceof IgnoreDefinition)) {
					link = definition;
				}
				break;
			}

			// Iterate fields
			Field[] fields = type.getDeclaredFields();
			for (Field field : fields) {
				int modifiers = field.getModifiers();
				if (!Modifier.isStatic(modifiers)) {
					if (field.getType().isPrimitive()) {
						size += sizeOfPrimitive(field.getType());
						primitiveFieldList.add(field);
					} else {
						size += MemoryUtil.SIZE_OF_NULL_POINTER;
						if (!definitionMap.shouldIgnoreField(field)) {
							field.setAccessible(true);
							objectFieldList.add(field);
						}
					}
				}
			}
			type = type.getSuperclass();
		}

		// Done
		this.objectFields = objectFieldList.toArray(new Field[objectFieldList.size()]);
		this.primitiveFields = primitiveFieldList.toArray(new Field[primitiveFieldList.size()]);
		this.size = size;
	}

	/**
	 * Returns the class.
	 * @return the class.
	 */
	public final Class<I> getClazz() {
		return clazz;
	}

	@Override
	public long sizeOf(I instance) {
		if (link != null) {
			return link.sizeOf(instance) + size;
		}
		return size;
	}

	@Override
	public final boolean hasElements(I instance) {
		if (link != null && link.hasElements(instance)) {
			return true;
		}
		return objectFields.length > 0;
	}

	@Override
	public long sizeOfElements(I instance, ClassDefinitionMap definitionMap, Set<Object> instanceSet, long size) {
		if (link != null) {
			size = link.sizeOfElements(instance, definitionMap, instanceSet, size);
			// extends class, so only one instance
			size -= MemoryUtil.SIZE_OF_OBJECT_INSTANCE;
		}
		for (Field field : objectFields) {
            Object value = null;
            try {
                value = field.get(instance);
            } catch (IllegalAccessException e) {

            }
            if (value != null) {
				size += MemoryUtil.sizeOf(value, definitionMap, instanceSet);
			}
		}
		return size;
	}

	@Override
	public long sizeOfDebug(I instance, ClassDefinitionMap definitionMap, Set<Object> instanceSet, PrintStream stream) throws IllegalAccessException {
		long objectSize = MemoryUtil.SIZE_OF_OBJECT_INSTANCE;
		for (Field field : primitiveFields) {
			long fieldSize = sizeOfPrimitive(field.getType());
			stream.println(field.getDeclaringClass().getSimpleName() + "." + field.getName() + " " + fieldSize + " bytes");
		}
		if (link != null) {
			objectSize = link.sizeOfDebug(instance, definitionMap, instanceSet, stream);
		}
		for (Field field : objectFields) {
			Object value = field.get(instance);
			if (value != null) {
				long fieldSize = MemoryUtil.sizeOfDebug(value, definitionMap, instanceSet, stream);
				stream.println(field.getDeclaringClass().getSimpleName() + "." + field.getName() + " " + fieldSize + " bytes");
				objectSize += fieldSize;
			}
		}
		stream.println(instance.getClass().getSimpleName() + " " + objectSize + " bytes");
		return objectSize;
	}
}
